#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
获取文件类型提示词
根据指定的文件类型获取对应的提示词配置
支持先通用识别，再根据识别到的文件类型进行详细处理
"""

import os
from pathlib import Path
import sys
import importlib.util
import base64
import mimetypes
import time
import uuid
import json
import re
import logging
from typing import Dict, Any, Optional, Tuple, List, Union
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException, status, Form, UploadFile, File
from openai import OpenAI
from PIL import Image
import io
import shutil
import tempfile
import requests
from urllib.parse import urljoin

from py_files.utils.map_camel import convert_json_result_en_to_camel_case
# 从配置模块导入函数
from py_files.config import get_client, select_model
import base64
import mimetypes
from typing import Union, IO, Dict, Any

# 图像处理API模型和路由(大模型调用的是/v1/chat/completions)
# 创建路由
image_agent_router = APIRouter(prefix="/image_agent_generate", tags=["Image Agent API"])

# 配置日志记录
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)

# 创建日志记录器
log = logging.getLogger('image_agent')
log.setLevel(logging.INFO)

# 创建控制台处理器
console_handler = logging.StreamHandler(sys.stdout)  # 确保输出到标准输出
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))

# 创建文件处理器
file_handler = logging.FileHandler('image_agent.log')
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))

# 添加处理器到日志记录器
log.addHandler(console_handler)
log.addHandler(file_handler)

# 确保日志不会传播到根日志记录器，避免重复输出
log.propagate = False

# 测试日志是否正常工作
log.info("日志系统初始化完成")


json_prompt_cn_en1="""
提取图片中的信息为JSON格式:
    {'json_result_cn':{'中文键':'值'},'json_result_en':{'en_key':'值'}}
    
    要求:
    1. 中文键名用中文，英文键名用小写加下划线
    2. 中英文键一一对应，保持值一致
    3. 提取所有关键信息，保留原始格式和单位
    4. 不存在的信息或空位置用null。
    5. 对于数字,必须严格保留原始格式、小数位数和单位，禁止截断或四舍五入，保持数值精度完全一致；
    
    示例:
    {'json_result_cn':{'姓名':'张三'},'json_result_en':{'name':'张三'}}
    """
    
invoice_promt1= """
    发票字段定义(所有字段必须包含):
    "machineNo": {"description": "机器号", "example": ["",""]},
    "invoiceNo": {"description": "发票号码", "example": ["10043526",""]},
    "invoiceType": {"description": "发票票种", "example": ["专票",""]},
    "specificBusinessTypes": {"description": "特定业务类型", "example": ["建筑服务",""]},
    "invoicingDate": {"description": "开票日期", "example": ["2015年09月22日",""]},
    "purchaserName": {"description": "购买方名称", "example": ["天津公司",""]},
    "purchaserCode": {"description": "购买方纳税人识别号", "example": ["91120116MA074AJDXP",""]},
    "sellerName": {"description": "销售方名称", "example": ["天津科技公司",""]},
    "sellerCode": {"description": "销售方纳税人识别号", "example": ["91120116MA07J8594A",""]},
    "tradeName": {"description": "货物名称", "example": ["*信息技术服务*技术服务费",""]},
    "model": {"description": "规格型号", "example": ["",""]},
    "unit": {"description": "单位", "example": ["项",""]},
    "number": {"description": "数量", "example": ["",""]},
    "unitPrice": {"description": "单价", "example": ["",""]},
    "totalPrice": {"description": "金额", "example": ["95439.62",""]},
    "taxRate": {"description": "税率", "example": ["6%",""]},
    "taxAmount": {"description": "税额", "example": ["5726.38",""]},
    "totalAmount": {"description": "合计金额", "example": ["95439.62",""]},
    "totalTax": {"description": "合计税额", "example": ["5726.38",""]},
    "payee": {"description": "收款人", "example": ["张三",""]},
    "reviewer": {"description": "复核", "example": ["李四",""]},
    "drawer": {"description": "开票人", "example": ["王五",""]},
    "totalBig": {"description": "价税合计(大写)", "example": ["壹拾万壹仟壹佰陆拾陆圆整",""]},
    "totalSmall": {"description": "价税合计(小写)", "example": ["101166",""]},
    "remark": {"description": "备注", "example": ["",""]}
    """
    
json_prompt_cn_en2="""
精确提取图片中的信息为结构化JSON格式（仅需中文输出），严格遵循以下规范：\n\n{\n    \"json_result_cn\": {\"中文键\":\"值\"}\n}\n\n■ 强制要求：\n1. 键名使用标准中文描述（如：\"姓名\"、\"身份证号\"）\n\n2. 数值处理：\n   - 所有数字必须完整保留原始精度：\n     ✓ 禁止任何截断（如\"3.1400\"必须保持完整）\n     ✓ 禁止四舍五入（如\"2.678\"不得改为\"2.68\"）\n     ✓ 禁止科学计数法转换\n     ✓ 必须保留前导零/后缀零（如\"007.500\"）\n   - 连带单位必须完整保留（如\"5.20kg\"）\n\n3. 完整性规则：\n   - 不存在的信息必须显式标注null\n   - 保持原始文本格式（包括特殊符号、换行符等）\n   - 金额/编号等特殊数字必须保留原始格式（如\"¥1,200.00\"）

4. 值格式要求：
   - ⚠️ 重要：所有字段的值必须是数组格式，即使只有一个值也要用[\"值\",\"\"]表示
   - 例如：\"温度\": [\"-12.500℃\",\"\"]，而不是 \"温度\": \"-12.500℃\"
   - 空值应表示为：\"字段\": [\"\",\"\"]

■ 示例：
正确输出：
{
    \"json_result_cn\": {
        \"温度\": [\"-12.500℃\",\"\"], 
        \"账户余额\": [\"¥1,234.56\",\"\"],
        \"序列号\": [\"A001-2024\",\"\"]
    }
}

错误输出：
{
    \"json_result_cn\": {
        \"温度\": \"-12.5℃\",  // 错误：截断小数位且不是数组格式
        \"账户余额\": \"1234.56\"  // 错误：丢失货币符号和千分位且不是数组格式
    }
}
"""  

invoice_promt2= """
    发票字段定义(所有字段必须包含):json_result_cn结果保留。且将json_result_en中的中文键映射到下列英文，结果保存在json_result_en，不存在的内容填null，注意禁止任何截断（如\"3.1400\"必须保持完整）\n     ✓ 禁止四舍五入（如\"2.678\"不得改为\"2.68\"）：
    ⚠️ 重要：所有字段的值必须是数组格式，即使只有一个值也要用["值",""]表示，第二个值可以为空字符串。例如["10043526",""]、["",""]:
    "machineNo": {"description": "机器号", "example": ["",""]},
    "invoiceNo": {"description": "发票号码", "example": ["10043526",""]},
    "invoiceType": {"description": "发票票种", "example": ["专票",""]},
    "specificBusinessTypes": {"description": "特定业务类型", "example": ["建筑服务",""]},
    "invoicingDate": {"description": "开票日期", "example": ["2015年09月22日",""]},
    "purchaserName": {"description": "购买方名称", "example": ["天津公司",""]},
    "purchaserCode": {"description": "购买方纳税人识别号", "example": ["91120116MA074AJDXP",""]},
    "sellerName": {"description": "销售方名称", "example": ["天津科技公司",""]},
    "sellerCode": {"description": "销售方纳税人识别号", "example": ["91120116MA07J8594A",""]},
    "tradeName": {"description": "货物名称", "example": ["*信息技术服务*技术服务费",""]},
    "model": {"description": "规格型号", "example": ["",""]},
    "unit": {"description": "单位", "example": ["项",""]},
    "number": {"description": "数量", "example": ["",""]},
    "unitPrice": {"description": "单价", "example": ["",""]},
    "totalPrice": {"description": "金额", "example": ["95439.62",""]},
    "taxRate": {"description": "税率", "example": ["6%",""]},
    "taxAmount": {"description": "税额", "example": ["5726.38",""]},
    "totalAmount": {"description": "合计金额", "example": ["95439.62",""]},
    "totalTax": {"description": "合计税额", "example": ["5726.38",""]},
    "payee": {"description": "收款人", "example": ["张三",""]},
    "reviewer": {"description": "复核", "example": ["李四",""]},
    "drawer": {"description": "开票人", "example": ["王五",""]},
    "totalBig": {"description": "价税合计(大写)", "example": ["壹拾万壹仟壹佰陆拾陆圆整",""]},
    "totalSmall": {"description": "价税合计(小写)", "example": ["101166",""]},
    "remark": {"description": "备注", "example": ["",""]}
    
    注意：必须将每个字段的值都设置为数组格式，例如"machineNo": ["值",""]，即使值为空也要返回["",""]。
    """
    

# 提示词映射配置
agent_prompt_type_mappings={
    "通用json":
        {"prompt":json_prompt_cn_en2 },
    "发票":
        {"prompt": invoice_promt2}
    
}

from fastapi import FastAPI, HTTPException
import httpx
from pydantic import BaseModel
import logging

app = FastAPI()

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class ImageUrlRequest(BaseModel):
    image_url: str  # 接收要测试的图片URL

@image_agent_router.post("/test_image_url")
async def test_image_url(request: ImageUrlRequest):
    """
    测试图片URL的可访问性
    参数:
        image_url (str): 要测试的图片URL，例如 "http://192.168:8007/xxx.png"
    返回:
        JSON: 包含状态码、响应时间(ms)和错误信息（如果有）
    """
    url = request.image_url
    timeout = httpx.Timeout(10.0)  # 设置10秒超时
    try:
        async with httpx.AsyncClient() as client:
            # 发送HEAD请求（只获取头部信息，不下载完整内容）
            start_time = time.time()
            resp = await client.head(url, timeout=timeout)
            elapsed_ms = (time.time() - start_time) * 1000  # 计算响应时间(毫秒)
            
            # 如果HEAD不被支持，改用GET请求（但只读取前1KB内容）
            if resp.status_code == 405:
                resp = await client.get(url, timeout=timeout)
                await resp.aclose()  # 立即关闭连接，避免下载完整内容
            
            logger.info(f"测试URL: {url} | 状态码: {resp.status_code} | 耗时: {elapsed_ms:.2f}ms")
            return {
                "status": "success",
                "status_code": resp.status_code,
                "response_time_ms": elapsed_ms,
                "content_type": resp.headers.get("content-type"),
                "content_length": resp.headers.get("content-length")
            }
    except httpx.ConnectError:
        logger.error(f"连接失败: {url} (网络不可达)")
        raise HTTPException(status_code=502, detail="无法连接到图片服务器")
    except httpx.TimeoutException:
        logger.error(f"请求超时: {url} (超过{timeout}秒)")
        raise HTTPException(status_code=504, detail="图片服务器响应超时")
    except Exception as e:
        logger.error(f"测试失败: {url} | 错误: {str(e)}")
        raise HTTPException(status_code=500, detail=f"图片URL测试失败: {str(e)}")
    
# 基础工具函数
def get_file_type_mappings():
    """返回所有可用的文件类型映射"""
    return agent_prompt_type_mappings

def get_available_agent_types() -> list:
    """获取所有可用的智能体类型列表"""
    return list(get_file_type_mappings().keys())

def get_prompt_by_type(file_type: str="发票") -> Optional[Dict[str, Any]]:
    """根据文件类型获取相应的提示词配置"""
    file_type_mappings = get_file_type_mappings()
    if file_type in file_type_mappings:
        return file_type_mappings[file_type]
    else:
        log.warning(f"未找到文件类型 '{file_type}' 的配置")
        return None

def get_agent_by_type(agent_type: str) -> Optional[str]:
    """根据智能体类型获取对应的提示词"""
    mappings = get_file_type_mappings()
    if agent_type in mappings:
        return mappings[agent_type]["prompt"]
    else:
        log.warning(f"未找到智能体类型 '{agent_type}' 的配置")
        return None

def get_prompt_by_type_add(agent_type: str) -> Optional[str]:
    """获取完整提示词，通用json为基础，其他类型为补充"""
    prompt = get_agent_by_type("通用json")
    # 以通用json的提示词为基础提示词，其他提示词仅定义输出形式
    if agent_type != "通用json":
        prompt_cur = get_agent_by_type(agent_type)
        prompt = prompt + prompt_cur
    return prompt

def image_to_data_url(file_path):
    """将图片文件转换为data URL格式"""
    log.debug(f"转换图片为data URL: {file_path}")
    # 获取文件的 MIME 类型（根据扩展名）
    mime_type, _ = mimetypes.guess_type(file_path)
    if not mime_type or not mime_type.startswith('image/'):
        log.error(f"不支持的图片格式: {file_path}")
        raise ValueError("Unsupported image format")

    # 读取文件并编码为 Base64
    with open(file_path, "rb") as image_file:
        base64_data = base64.b64encode(image_file.read()).decode("utf-8")

    return f"data:{mime_type};base64,{base64_data}"

# 老版的image_url格式不含{"url:"},新版的包含
def image_to_data_url_by_model(file_path, model_name):
    # log.debug(f"转换图片为data URL: {file_path}")
    # 获取文件的 MIME 类型（根据扩展名）
    mime_type, _ = mimetypes.guess_type(file_path)
    if not mime_type or not mime_type.startswith('image/'):
        raise ValueError("Unsupported image format")

    # 读取文件并编码为 Base64
    with open(file_path, "rb") as image_file:
        base64_data = base64.b64encode(image_file.read()).decode("utf-8")
        
    data_url = f"data:{mime_type};base64,{base64_data}"
    # image_url判断-图片类型
    no_url_model_list = ["qwen_31132_244"] 
    if model_name not in no_url_model_list:
        # 符合 "image_url": {"url": "data:..."}
        return {"url": data_url}
    else:
        # 直接返回 "data:..."
        return data_url

def image_url_by_model(image_url, model_name,no_url_model_list = ["qwen_31132_244"] ):
    # 根据模型类型，返回data_url或{"url": data_url
    # image_url判断-图片类型
    if model_name not in no_url_model_list:
        # 符合 "image_url": {"url": "data:..."}
        return {"url": image_url}
    else:
        # 直接返回 "data:..."
        return image_url
    

def imagepath_file_to_data_url_by_model(
    file_path: Union[str, IO[bytes]], 
    model_name: str
) -> Union[str, Dict[str, str]]:
    """
    将图片文件路径或文件对象转换为 Data URL
    优点：能同时处理文件路径或文件对象；
    Args:
        file_path: 图片文件路径(str)或已打开的文件对象(IO[bytes])
        model_name: 模型名称，用于决定返回格式
        
    Returns:
        如果是支持URL的模型，返回 {"url": "data:..."}
        否则直接返回 "data:..."
        
    Raises:
        ValueError: 如果文件不是图片格式
        IOError: 如果文件读取失败
    """
    # 获取 MIME 类型
    if isinstance(file_path, str):
        # 如果是字符串路径
        mime_type, _ = mimetypes.guess_type(file_path)
        if not mime_type or not mime_type.startswith('image/'):
            raise ValueError("Unsupported image format")
        
        # 读取文件
        with open(file_path, "rb") as image_file:
            base64_data = base64.b64encode(image_file.read()).decode("utf-8")
    else:
        # 如果是文件对象
        file_data = file_path.read()
        # 尝试获取文件名获取MIME类型
        if hasattr(file_path, 'name'):
            mime_type, _ = mimetypes.guess_type(file_path.name)
        else:
            # 如果没有文件名，使用通用图片类型
            mime_type = 'image/jpeg'
        
        if not mime_type or not mime_type.startswith('image/'):
            raise ValueError("Unsupported image format")
            
        base64_data = base64.b64encode(file_data).decode("utf-8")
    
    data_url = f"data:{mime_type};base64,{base64_data}"
    
    # 根据模型决定返回格式
    no_url_model_list = ["qwen_31132_244"] 
    if model_name not in no_url_model_list:
        return {"url": data_url}
    else:
        return data_url
    
def imageurl_by_model(data_url, model_name, no_url_model_list=["qwen_31132_244"]):
    """
    根据模型返回合适的 image_url 格式。
    
    Args:
        data_url (str): 图片的 URL 或 base64 数据（如 "data:image/png;base64,..."）。
        model_name (str): 调用的模型名称。
        no_url_model_list (list): 需要直接返回 data_url 的模型列表。
    
    Returns:
        dict or str: 返回 {"url": data_url} 或直接返回 data_url。
    """
    if model_name in no_url_model_list:
        # 直接返回 data_url（适用于 qwen_31132_244 等模型）
        return data_url
    else:
        # 返回 {"url": data_url}（适用于大多数模型）
        return {"url": data_url}
    
def _fix_truncated_json(content):
    """修复被截断的JSON"""
    # 计算需要添加的右括号数量
    missing_braces = content.count('{') - content.count('}')
    if missing_braces > 0:
        # 添加缺失的右括号
        content += '}' * missing_braces
    return content

def _extract_json_from_content(content):
    """从内容中提取JSON，处理可能包含在代码块中的情况"""
    # 如果内容包含```json标记，提取其中的JSON部分
    if "```json" in content:
        # 提取```json和```之间的内容
        json_content = content.split("```json")[1].split("```")[0].strip()
    elif "```" in content:
        # 提取```和```之间的内容
        json_content = content.split("```")[1].split("```")[0].strip()
    else:
        json_content = content.strip()
    
    return json_content.strip()

def parse_json_content(content):
    """解析内容中的JSON，处理可能嵌套的情况"""
    try:
        # 处理可能包含在代码块中的JSON
        json_content = _extract_json_from_content(content)
        # 然后解析为JSON对象
        json_result = json.loads(json_content)
        
        # 如果json_result包含content字段，并且content是字符串格式的JSON
        if "content" in json_result and isinstance(json_result["content"], str):
            try:
                # 尝试从content中提取JSON
                extracted_content = _extract_json_from_content(json_result["content"])
                nested_json = json.loads(extracted_content)
                # 如果成功解析，使用嵌套的JSON替换json_result
                return nested_json
            except json.JSONDecodeError:
                # 如果解析失败，保持原样
                pass
        return json_result
    except json.JSONDecodeError:
        # 如果不是有效的JSON，将原始内容包装为JSON返回
        return {
            "content": content
        }

def process_image_path_with_prompt(local_image_path: str, model_name=select_model, prompt_type: str = "发票", max_retries: int = 1, timeout: int = 120):
    """使用OpenAI模型处理图片并根据指定的提示类型生成描述
    
    参数:
        local_image_path: 本地图像文件路径
        model_name: 要使用的模型名称
        prompt_type: 提示类型
        max_retries: 最大重试次数，默认为1（不重试）
        timeout: 请求超时时间（秒）
    """
    start_time = time.time()
    log.info(f"开始处理图片: {local_image_path}, 提示类型: {prompt_type}, 模型: {model_name}")
    
    try:
        data_url = image_to_data_url_by_model(local_image_path, model_name)
        client, model_name_internal = get_client(model_name)
        prompt = get_prompt_by_type_add(prompt_type)
        
        log.info(f"图片处理中，超时设置为 {timeout} 秒，禁用了自动重试")
        completion = client.chat.completions.create(
            model=model_name_internal,
            messages=[
                # 系统提示词
                {"role": "system", "content": prompt},
                # 用户输入（图片+问题）
                {"role": "user", "content": [
                    {"type": "text", "text": "按照系统提示的内容提取图片中的json信息"},
                    {"type": "image_url", "image_url": data_url},
                ]},
            ],
            max_tokens=1024,
            stream=False,
            timeout=timeout  # 设置超时时间
        )
        end_time = time.time()
        processing_time = end_time - start_time
        log.info(f"图片识别完成: {local_image_path}，耗时 {processing_time:.2f} 秒")
        return completion
    except Exception as e:
        end_time = time.time()
        processing_time = end_time - start_time
        log.error(f"图片处理失败: {local_image_path}，耗时 {processing_time:.2f} 秒，错误: {str(e)}")
        raise

# 修改上传文件夹定义（移到文件前面）
UPLOAD_FOLDER = 'uploads'
TEMP_FOLDER = os.path.join(UPLOAD_FOLDER, 'temp')

# 确保上传和临时文件夹存在
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)
if not os.path.exists(TEMP_FOLDER):
    os.makedirs(TEMP_FOLDER)

# 修改process_image_url_with_prompt函数中的requests部分，确保正确下载到临时目录
def process_image_url_with_prompt(image_url: str, model_name=select_model, prompt_type: str = "发票", max_retries: int = 1, timeout: int = 120):
    """使用OpenAI模型处理图片URL并根据指定的提示类型生成描述
    
    参数:
        image_url: 图片URL
        model_name: 要使用的模型名称
        prompt_type: 提示类型
        max_retries: 最大重试次数，默认为1（不重试）
        timeout: 请求超时时间（秒）
    """
    start_time = time.time()
    # 截断URL用于日志显示
    url_for_log = image_url[:50] + ('...' if len(image_url) > 50 else '')
    log.info(f"开始处理图片URL: {url_for_log}, 提示类型: {prompt_type}, 模型: {model_name}")
    
    try:
        image_url_data = imageurl_by_model(image_url, model_name)
        # 获取客户端和模型名称
        client, model_name_internal = get_client(model_name)  
        # 获取提示词
        prompt = get_prompt_by_type_add(prompt_type)
        # 构建API请求参数
        messages = [
            # 系统提示词
            {"role": "system", "content": prompt},
            # 用户输入（图片+问题）
            {"role": "user", "content": [
                {"type": "text", "text": "按照系统提示的内容提取图片中的json信息"},
                {"type": "image_url", "image_url": image_url_data},
            ]},
        ]
        # 尝试调用API
        completion = client.chat.completions.create(
            model=model_name_internal,
            messages=messages,
            max_tokens=1024,
            stream=False,
            timeout=timeout  # 设置超时时间
        )
        
        end_time = time.time()
        processing_time = end_time - start_time
        log.info(f"图片URL识别完成: {url_for_log}，耗时 {processing_time:.2f} 秒")
        return completion           
            
    except Exception as e:
        end_time = time.time()
        processing_time = end_time - start_time
        log.error(f"图片URL处理失败: {url_for_log}，耗时 {processing_time:.2f} 秒，错误: {str(e)}")
        raise

async def process_image_file_with_prompt(image_file: UploadFile, model_name=select_model, prompt_type: str = "发票", timeout: int = 120):
    """使用OpenAI模型直接处理上传的图片文件并根据指定的提示类型生成描述
    
    参数:
        image_file: 上传的文件对象
        model_name: 要使用的模型名称
        prompt_type: 提示类型
        timeout: 请求超时时间（秒）
    """
    start_time = time.time()
    log.info(f"开始直接处理上传的图片文件: {image_file.filename}, 提示类型: {prompt_type}, 模型: {model_name}")
    
    try:
        # 将上传的文件转换为data URL
        data_url = await image_file2base_url(image_file)
        
        # 将data URL转换为符合模型需要的格式
        image_url_data = imageurl_by_model(data_url, model_name)
        
        # 获取客户端和模型名称
        client, model_name_internal = get_client(model_name)
        
        # 获取提示词
        prompt = get_prompt_by_type_add(prompt_type)
        
        log.info(f"图片处理中，超时设置为 {timeout} 秒")
        completion = client.chat.completions.create(
            model=model_name_internal,
            messages=[
                # 系统提示词
                {"role": "system", "content": prompt},
                # 用户输入（图片+问题）
                {"role": "user", "content": [
                    {"type": "text", "text": "按照系统提示的内容提取图片中的json信息"},
                    {"type": "image_url", "image_url": image_url_data},
                ]},
            ],
            # max_tokens=2048,
            stream=False,
            timeout=timeout  # 设置超时时间
        )
        end_time = time.time()
        processing_time = end_time - start_time
        log.info(f"图片文件识别完成: {image_file.filename}，耗时 {processing_time:.2f} 秒")
        return completion
    except Exception as e:
        end_time = time.time()
        processing_time = end_time - start_time
        log.error(f"图片文件处理失败: {image_file.filename}，耗时 {processing_time:.2f} 秒，错误: {str(e)}")
        raise

def resize_image_if_needed(image_path: str, max_size: int = 4000) -> str:
    """
    如果图像太大，将其调整到合适的大小
    
    参数:
        image_path: 原始图像路径
        max_size: 图像的最大尺寸（宽或高）
        
    返回:
        调整后的图像路径（如果需要调整）或原始路径（如果不需要调整）
    """
    try:
        # 打开图像
        with Image.open(image_path) as img:
            width, height = img.size
            
            # 检查图像是否需要调整大小
            if width > max_size or height > max_size:
                # 计算调整比例
                ratio = min(max_size / width, max_size / height)
                new_width = int(width * ratio)
                new_height = int(height * ratio)
                
                log.info(f"图像尺寸过大 ({width}x{height})，调整为 ({new_width}x{new_height})")
                
                # 调整图像大小
                resized_img = img.resize((new_width, new_height), Image.LANCZOS)
                
                # 创建调整后的图像的路径
                filename, ext = os.path.splitext(image_path)
                resized_path = f"{filename}_resized{ext}"
                
                # 保存调整后的图像
                resized_img.save(resized_path, quality=95)
                
                return resized_path
            else:
                log.info(f"图像尺寸合适 ({width}x{height})，无需调整")
                return image_path
    except Exception as e:
        log.error(f"调整图像大小时出错: {str(e)}")
        # 出错时返回原始路径
        return image_path


async def process_image_url_safely(image_url: str, model_name: str, prompt_type: str, timeout: int = 120) -> dict:
    """
    安全处理图片URL，如果直接处理失败，会尝试转换为base64后重试
    
    参数:
        image_url: 图片URL
        model_name: 模型名称
        prompt_type: 提示类型
        timeout: 超时时间（秒）
        
    返回:
        dict: API响应结果
        
    异常:
        HTTPException: 如果处理失败
    """
    # 截断URL用于日志显示
    url_for_log = image_url[:50] + ('...' if len(image_url) > 50 else '')
    log.info(f"开始安全处理图片URL: {url_for_log}")
    
    # 确保临时目录存在
    if not os.path.exists(TEMP_FOLDER):
        os.makedirs(TEMP_FOLDER)
        log.info(f"创建临时目录: {TEMP_FOLDER}")
    
    try:
        # 首先尝试直接使用URL
        try:
            log.info(f"尝试直接使用URL处理图片: {url_for_log}")
            response = process_image_url_with_prompt(
                image_url=image_url,  # 传递完整URL，不要使用切片
                model_name=model_name,
                prompt_type=prompt_type,
                timeout=timeout
            )
            log.info("使用原始URL成功处理图片")
            return response
        
        except Exception as e:
            error_msg = str(e)
            log.warning(f"使用原始URL处理失败: {error_msg}，尝试转换为base64后重试")

            download_image_dict = await download_image_url(image_url)
            
            local_path, temp_dir = download_image_dict["local_path"], download_image_dict["temp_dir"]
            # 下载图片到本地临时文件
            try:
                # 使用base64重试
                log.info("使用base64格式重试处理图片")
                response = process_image_path_with_prompt(
                    local_image_path=local_path,  # 使用本地文件路径
                    model_name=model_name,
                    prompt_type=prompt_type,
                    timeout=timeout
                )
                log.info("使用base64格式成功处理图片")
                
                return response
            finally:
                await delete_temp_folder(temp_dir)
    
    except Exception as e:
        log.error(f"安全处理图片URL失败: {str(e)}")
        raise

# 图像处理请求模型
class ImageAgentRequest(BaseModel):
    agent_prompt_type: str = "通用json"
    image_file_path: str

# 图像处理响应模型
class ImageAgentResponse(BaseModel):
    id: str
    created: int
    agent_prompt_type: str = "通用json"
    json_result_cn: Dict[str, Any] = {}
    json_result_en: Dict[str, Any] = {}
    json_result_en_invoice: Optional[Dict[str, Any]] = None
    error: Optional[str] = None
    success: bool = True



# 清理临时文件夹的函数
def cleanup_temp_folder(max_age_hours=24):
    """
    清理临时文件夹中的旧文件
    
    参数:
        max_age_hours: 文件最大保存时间（小时）
    """
    try:
        if not os.path.exists(TEMP_FOLDER):
            return
            
        # 获取当前时间
        now = time.time()
        # 遍历临时文件夹中的所有文件
        for filename in os.listdir(TEMP_FOLDER):
            file_path = os.path.join(TEMP_FOLDER, filename)
            # 如果是文件且修改时间超过了最大保存时间
            if os.path.isfile(file_path) and os.path.getmtime(file_path) < now - max_age_hours * 3600:
                try:
                    os.unlink(file_path)
                    log.debug(f"已清理过期临时文件: {file_path}")
                except Exception as e:
                    log.warning(f"清理临时文件失败: {str(e)}")
    except Exception as e:
        log.error(f"清理临时文件夹时出错: {str(e)}")


# 添加新函数
async def image_file2base_url(image_file: UploadFile) -> str:
    """
    将上传的文件直接转换为base64编码的data URL，无需保存到本地文件
    
    
    参数:
        image_file: 上传的文件对象
    
    返回:
        str: 转换后的data URL (格式: )
    
    异常:
        HTTPException: 如果文件不是图片或处理失败
        ValueError: 如果文件内容为空
    """
    log.info(f"将上传的文件转换为data URL: {image_file.filename}")
    
    try:
        # 检查文件是否为图片
        content_type = image_file.content_type
        if not content_type or not content_type.startswith('image/'):
            log.error(f"文件不是图片: {image_file.filename}, 类型: {content_type}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="只接受图片文件"
            )
            
        # 读取文件内容
        file_content = await image_file.read()
        if not file_content:  # 检查文件内容是否为空
            raise ValueError("上传的文件内容为空")
            
        # 获取文件扩展名
        file_extension = os.path.splitext(image_file.filename)[1][1:].lower() if image_file.filename else ""
        if not file_extension:
            # 如果没有扩展名，根据内容类型猜测
            if content_type == 'image/jpeg':
                file_extension = 'jpg'
            elif content_type == 'image/png':
                file_extension = 'png'
            elif content_type == 'image/gif':
                file_extension = 'gif'
            elif content_type == 'image/webp':
                file_extension = 'webp'
            else:
                file_extension = 'jpg'  # 默认使用jpg
                
        # 构建MIME类型
        mime_type = content_type
        
        # 转换为base64编码
        base64_data = base64.b64encode(file_content).decode('utf-8')
        
        # 创建data URL
        data_url = f"data:{mime_type};base64,{base64_data}"
        
        log.info(f"文件已转换为data URL，类型: {mime_type}")
        return data_url
    
    except HTTPException:
        # 重新抛出HTTPException异常
        raise
    except Exception as e:
        error_msg = f"转换文件为data URL时出错: {str(e)}"
        log.error(error_msg)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=error_msg
        )

# 将上传的文件保存到本地并返回文件路径
async def image_file2path(image_file: UploadFile) -> str:
    """
    将上传的文件保存到本地临时目录并返回文件路径
    
    参数:
        image_file: 上传的文件对象
    
    返回:
        str: 保存后的本地文件路径
    
    异常:
        HTTPException: 如果文件处理失败
    """
    log.info(f"处理上传的文件: {image_file.filename}")
    
    try:
        # 检查文件是否为图片
        content_type = image_file.content_type
        if not content_type or not content_type.startswith('image/'):
            log.error(f"文件不是图片: {image_file.filename}, 类型: {content_type}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="只接受图片文件"
            )
        
        # 获取文件扩展名
        file_extension = os.path.splitext(image_file.filename)[1] if image_file.filename else ""
        if not file_extension:
            # 如果没有扩展名，根据内容类型猜测
            if content_type == 'image/jpeg':
                file_extension = '.jpg'
            elif content_type == 'image/png':
                file_extension = '.png'
            elif content_type == 'image/gif':
                file_extension = '.gif'
            elif content_type == 'image/webp':
                file_extension = '.webp'
            else:
                file_extension = '.jpg'  # 默认使用jpg
        
        # 生成唯一文件名
        unique_filename = f"{uuid.uuid4().hex}{file_extension}"
        
        # 文件保存路径
        file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
        
        # 保存上传的文件
        content = await image_file.read()
        with open(file_path, "wb") as buffer:
            buffer.write(content)
        
        log.info(f"文件已保存: {file_path}")
        return file_path
    
    except HTTPException:
        # 重新抛出HTTPException异常
        raise
    except Exception as e:
        error_msg = f"处理上传文件时出错: {str(e)}"
        log.error(error_msg)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=error_msg
        )

# 修改图像处理API路由定义
@image_agent_router.post("/get_image_file2json", response_model=None)
async def image_agent_generate(
    agent_prompt_type: str = Form("通用json"),
    image_file_path: UploadFile = File(...),
    model_name: str = Form(None),  # 改为None，在函数内部处理默认值
):
    """根据指定的智能体类型和图像文件生成处理结果"""
    
    # 处理model_name参数，如果为None则使用默认的select_model
    if model_name is None:
        model_name = select_model
    
    log.info(f"开始处理图像文件 {image_file_path.filename}, 类型 {agent_prompt_type}, 模型 {model_name}")
    
    try:
        # 验证智能体类型是否可用
        if agent_prompt_type not in get_available_agent_types():
            available_types = get_available_agent_types()
            log.error(f"未找到智能体类型: {agent_prompt_type}，可用类型: {', '.join(available_types)}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"未找到智能体类型 '{agent_prompt_type}'。可用类型: {', '.join(available_types)}"
            )
        # 调用 process_image_path_with_prompt 处理图像
        response = await process_image_file_with_prompt(
            image_file=image_file_path, 
            model_name=model_name,
            prompt_type=agent_prompt_type
        )
        
        # 提取结果
        content = response.choices[0].message.content
                # 构建响应
        result = {
            "agent_prompt_type": agent_prompt_type,
            "json_result_cn": {},
            "json_result_en": {},
            "success": True
        }
        
        # 解析JSON内容
        json_result = parse_json_content(content)
        
        # 将json_result的内容直接放入结果中，而不是作为一个嵌套字段（必须）
        if isinstance(json_result, dict):
            result.update(json_result)
            
        log.info(f"图像处理完成，返回标准格式")
        if True:#下划线转为驼峰 
            result=convert_json_result_en_to_camel_case(result)
        return result
    
    except HTTPException as e:
        # 重新抛出HTTPException异常
        log.error(f"请求处理出错(HTTP): {e.detail}")
        raise
    except Exception as e:
        error_msg = f"处理请求时发生错误: {str(e)}"
        log.error(f"请求处理出错: {error_msg}")
        # 直接抛出异常
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=error_msg
        )
    finally:
        # 清理临时调整大小的图像文件
        image_filepath = locals().get('image_filepath')
        original_path = locals().get('original_path')
        if image_filepath and original_path and original_path != image_filepath and os.path.exists(image_filepath):
            try:
                # 仅在原始路径和处理路径不同时执行清理
                if '_resized.' in image_filepath or '_resized' in image_filepath:
                    log.info(f"清理临时调整大小的图像文件: {image_filepath}")
                    os.remove(image_filepath)
            except Exception as e:
                log.warning(f"清理临时图像文件时出错: {str(e)}")

@image_agent_router.post("/get_image2local_json", response_model=None)
async def image_agent_generate(
    agent_prompt_type: str = Form("通用json"),
    image_file_path: UploadFile = File(...),
    model_name: str = Form(None),  # 改为None，在函数内部处理默认值
    max_image_size: int = Form(4000),  # 图像的最大尺寸（宽或高）
):
    """根据指定的智能体类型和图像文件生成处理结果;
    先将文件保存到本地，然后处理；
    """
    
    # 处理model_name参数，如果为None则使用默认的select_model
    if model_name is None:
        model_name = select_model
    
    log.info(f"开始处理图像文件 {image_file_path.filename}, 类型 {agent_prompt_type}, 模型 {model_name}")
    
    try:
        # 将上传的文件保存为本地文件
        image_filepath = await image_file2path(image_file_path)
        
        # 原始图像路径
        original_path = image_filepath
        
        # 验证智能体类型是否可用
        if agent_prompt_type not in get_available_agent_types():
            available_types = get_available_agent_types()
            log.error(f"未找到智能体类型: {agent_prompt_type}，可用类型: {', '.join(available_types)}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"未找到智能体类型 '{agent_prompt_type}'。可用类型: {', '.join(available_types)}"
            )
            
        # 检查图像是否需要调整大小
        if max_image_size > 0:
            image_filepath = resize_image_if_needed(image_filepath, max_image_size)
            if image_filepath != original_path:
                log.info(f"使用调整大小后的图像: {image_filepath}")
        
        # 使用get_prompt_by_type_add获取完整提示词（通用json + 特定类型）
        log.info(f"使用{agent_prompt_type}类型的提示词处理图像")
            
        # 调用 process_image_path_with_prompt 处理图像
        response = process_image_path_with_prompt(
            local_image_path=image_filepath, 
            model_name=model_name,
            prompt_type=agent_prompt_type
        )
        
        # 提取结果
        content = response.choices[0].message.content
                # 构建响应
        result = {
            "agent_prompt_type": agent_prompt_type,
            "json_result_cn": {},
            "json_result_en": {},
            "success": True
        }
        
        # 解析JSON内容
        json_result = parse_json_content(content)
        
        # 将json_result的内容直接放入结果中，而不是作为一个嵌套字段（必须）
        if isinstance(json_result, dict):
            result.update(json_result)
            
        log.info(f"图像处理完成，返回标准格式")
        if True:#下划线转为驼峰 
            result=convert_json_result_en_to_camel_case(result)
        return result
    
    except HTTPException as e:
        # 重新抛出HTTPException异常
        log.error(f"请求处理出错(HTTP): {e.detail}")
        raise
    except Exception as e:
        error_msg = f"处理请求时发生错误: {str(e)}"
        log.error(f"请求处理出错: {error_msg}")
        # 直接抛出异常
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=error_msg
        )
    finally:
        # 清理临时调整大小的图像文件
        image_filepath = locals().get('image_filepath')
        original_path = locals().get('original_path')
        if image_filepath and original_path and original_path != image_filepath and os.path.exists(image_filepath):
            try:
                # 仅在原始路径和处理路径不同时执行清理
                if '_resized.' in image_filepath or '_resized' in image_filepath:
                    log.info(f"清理临时调整大小的图像文件: {image_filepath}")
                    os.remove(image_filepath)
            except Exception as e:
                log.warning(f"清理临时图像文件时出错: {str(e)}")
                
async def download_image_to_temp(
    image_url: str,
    temp_dir: str = "uploads/temp"  # 默认路径，可被覆盖
) -> str:
    """
    将图片URL下载到指定临时目录，返回本地文件路径
    
    Args:
        image_url (str): 图片的HTTP/HTTPS URL
        temp_dir (str): 临时目录路径（默认为"uploads/temp"）
        
    Returns:
        str: 本地文件绝对路径（如 "/project/uploads/temp/xxx.jpg"）
    """
    # 转换为Path对象并解析绝对路径
    temp_dir_path = Path(temp_dir).absolute()
    temp_dir_path.mkdir(parents=True, exist_ok=True)
    
    # 生成唯一文件名（保留原始扩展名）
    file_ext = os.path.splitext(image_url)[1][:5]  # 限制扩展名长度
    filename = f"{uuid.uuid4().hex}{file_ext if file_ext else '.jpg'}"
    local_path = temp_dir_path / filename
    
    try:
        async with httpx.AsyncClient() as client:
            # 流式下载（支持大文件）
            async with client.stream("GET", image_url, timeout=30.0) as response:
                response.raise_for_status()  # 自动处理4xx/5xx错误
                
                # 写入文件
                with open(local_path, "wb") as f:
                    async for chunk in response.aiter_bytes(chunk_size=1024*1024):  # 1MB/块
                        f.write(chunk)
                        
        return str(local_path)
    
    except httpx.TimeoutException:
        raise HTTPException(status_code=504, detail="图片下载超时")
    except httpx.HTTPStatusError as e:
        raise HTTPException(status_code=502, detail=f"图片服务器返回错误: {e.response.status_code}")
    except Exception as e:
        # 如果失败，删除可能已存在的临时文件
        if local_path.exists():
            local_path.unlink(missing_ok=True)
        raise HTTPException(status_code=500, detail=f"图片下载失败: {str(e)}")

@image_agent_router.post("/download_image_url")
async def download_image_url(
    image_url: str,
    temp_dir: Optional[str] = None  # 可选参数
):
    """
    下载图片到指定目录
    
    参数:
        image_url: 图片URL
        temp_dir (可选): 自定义临时目录路径
    """
    try:
        local_path = await download_image_to_temp(
            image_url,
            temp_dir=temp_dir or "uploads/temp"  # 使用参数或默认值
        )
        return {
            "status": "success",
            "local_path": local_path,
            "temp_dir": os.path.dirname(local_path)
        }
    except HTTPException as e:
        return JSONResponse(
            content={"status": "error", "detail": e.detail},
            status_code=e.status_code
        )

async def delete_temp_folder(temp_dir: str) -> dict:
    """
    安全删除临时目录及其所有内容
    
    Args:
        temp_dir: 要删除的目录路径（支持相对/绝对路径）
        
    Returns:
        {"status": "success", "deleted_path": str}
        或
        {"status": "error", "detail": str}
        
    Raises:
        HTTPException: 当目录不存在或权限不足时
    """
    path = Path(temp_dir).absolute()
    
    # 安全检查
    if not path.exists():
        raise HTTPException(
            status_code=404,
            detail=f"目录不存在: {path}"
        )
    
    if not path.is_dir():
        raise HTTPException(
            status_code=400,
            detail=f"路径不是目录: {path}"
        )
    
    # 防止误删重要目录（验证目录名包含'temp'）
    if "temp" not in path.name.lower():
        raise HTTPException(
            status_code=403,
            detail="只能删除包含'temp'的目录"
        )
    
    try:
        # 实际删除操作
        shutil.rmtree(path)
        logging.info(f"已删除临时目录: {path}")
        return {
            "status": "success",
            "deleted_path": str(path)
        }
        
    except PermissionError as e:
        logging.error(f"权限不足: {path} - {str(e)}")
        raise HTTPException(
            status_code=403,
            detail=f"删除失败: 权限不足 ({str(e)})"
        )
    except Exception as e:
        logging.error(f"删除失败: {path} - {str(e)}")
        raise HTTPException(
            status_code=500,
            detail=f"删除失败: {str(e)}"
        )
          
# 修改image_url2json函数，在调用API前先验证URL
@image_agent_router.post("/image_url2json", response_model=None)
async def image_url_agent_generate(
    agent_prompt_type: str = Form("通用json"),
    image_url: str = Form(None),  # 改为可选参数
    model_name: str = Form(None),  # 改为None，在函数内部处理默认值
):
    """识别图片的url->json识别结果
    
    参数:
        agent_prompt_type: 智能体类型，默认为"通用json"
        image_url: 图片URL
        model_name: 要使用的模型名称，如果为None则使用默认的select_model
        skip_url_validation: 是否跳过URL验证，默认为False
    """
    req_id = f"imageagent-{uuid.uuid4()}"
    
    # 处理model_name参数，如果为None则使用默认的select_model
    if model_name is None:
        model_name = select_model
    
    # 检查图片URL是否提供
    if image_url is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="必须提供image_url参数"
        )
    
    # 确保临时目录存在
    if not os.path.exists(TEMP_FOLDER):
        os.makedirs(TEMP_FOLDER)
        log.info(f"创建临时目录: {TEMP_FOLDER}")
    
    # 截断URL用于日志显示
    url_for_log = image_url[:50] + ('...' if len(image_url) > 50 else '')
    log.info(f"开始处理请求 {req_id}: 图像URL {url_for_log}, 类型 {agent_prompt_type}, 模型 {model_name}")
    
    try:
        # 验证智能体类型是否可用
        if agent_prompt_type not in get_available_agent_types():
            available_types = get_available_agent_types()
            log.error(f"未找到智能体类型: {agent_prompt_type}，可用类型: {', '.join(available_types)}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"未找到智能体类型 '{agent_prompt_type}'。可用类型: {', '.join(available_types)}"
            )
        
        
        # 使用安全处理函数处理图像URL
        log.info(f"使用{agent_prompt_type}类型的提示词处理图像URL")
        content = None
        try:
            # 调用process_image_url_safely函数处理图像URL
            log.info(f"调用process_image_url_safely函数处理图像URL: {url_for_log}")
            response = await process_image_url_safely(
                image_url=image_url,  # 传递完整URL，不要使用切片
                model_name=model_name,
                prompt_type=agent_prompt_type,
                timeout=120  # 设置超时时间
            )
            
            # 提取结果
            content = response.choices[0].message.content
            log.info(f"成功获取API响应内容，长度: {len(content)}")
            
        except Exception as api_error:
            error_details = str(api_error)
            log.error(f"API调用失败: {error_details}")
            
            # 详细分析错误类型
            if "Invalid URL" in error_details or "Failed to fetch URL" in error_details:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail=f"图片URL无效或无法访问: {error_details}"
                )
            elif "timed out" in error_details.lower():
                raise HTTPException(
                    status_code=status.HTTP_504_GATEWAY_TIMEOUT,
                    detail=f"API请求超时: {error_details}"
                )
            elif "rate limit" in error_details.lower():
                raise HTTPException(
                    status_code=status.HTTP_429_TOO_MANY_REQUESTS,
                    detail=f"API请求频率限制: {error_details}"
                )
            elif "unhashable type" in error_details.lower():
                raise HTTPException(
                    status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                    detail=f"处理URL时出错(unhashable type): {error_details}"
                )
            else:
                # 其他API错误
                raise HTTPException(
                    status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                    detail=f"API调用失败: {error_details}"
                )
        
        # 构建响应
        result = {
            "id": req_id,
            "created": int(time.time()),
            "agent_prompt_type": agent_prompt_type,
            "json_result_cn": {},
            "json_result_en": {},
            "success": True
        }
        
        # 解析JSON内容
        if content:
            json_result = parse_json_content(content)
            
            # 将json_result的内容直接放入结果中，而不是作为一个嵌套字段
            if isinstance(json_result, dict):
                result.update(json_result)
        
        log.info(f"请求 {req_id} 处理完成")
        
        # 清理临时文件夹中的旧文件
        try:
            cleanup_temp_folder(1)  # 清理1小时前的临时文件
        except Exception as e:
            log.warning(f"清理临时文件夹失败: {str(e)}")
            
        if True:  # 下划线转为驼峰 
            result = convert_json_result_en_to_camel_case(result)
        return result
    
    except HTTPException as e:
        # 重新抛出HTTPException异常
        log.error(f"请求 {req_id} 处理出错(HTTP): {e.detail}")
        raise
    except Exception as e:
        error_msg = f"处理请求时发生错误: {str(e)}"
        log.error(f"请求 {req_id} 处理出错: {error_msg}")
        # 直接抛出异常
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=error_msg
        )
    finally:
        # 确保在请求结束后清理临时文件
        try:
            cleanup_temp_folder(1)  # 清理1小时前的临时文件
        except Exception as e:
            log.warning(f"清理临时文件夹失败: {str(e)}")
